package devcken.net.socket;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

import org.apache.derby.iapi.util.ByteArray;

import devcken.runtime.EventHandler;
import devcken.utility.BitConverter;
import devcken.utility.GuidUtil;

public final class TcpClient
{
	/**
	 * 기본 버퍼 크기 상수를 나타낸다.
	 */
	public static final int _DEFAULT_BUFFER_SIZE = 8192;
	
	/**
	 * 통신을 위한 소켓을 나타낸다.
	 */
	private AsynchronousSocketChannel _socket;
	
	/**
	 * 소켓의 고유 코드 정보를 나타낸다.
	 */
	private String _id;
	
	/**
	 * IP 주소를 나타낸다.
	 */
	private InetAddress _address;
	
	/**
	 * 포트 번호를 나탄낸다.
	 */
	private int _port;
	
	/**
	 * 소켓이 메모리에 바이너리를 정렬하는 방식을 나타낸다.
	 */
	private ByteOrder _endian;
	
	/**
	 * 현재 수신 중인 데이터의 패킷 헤더를 나타낸다.
	 */
	private Header _currentHeader;
	
	/**
	 * 데이터 수신을 위한 버퍼 스크림을 나타낸다.
	 */
	private ByteArrayOutputStream _bufferStream;
	
	/**
	 * 전체 데이터 중 수진된 데이터의 위치를 나타낸다.
	 */
	private int _receivedOffset;
	
	/**
	 * 데이터 중 한 조각의 버퍼 사이즈를 나타낸다.
	 */
	protected int _pieceBufferSize;
	
	/**
	 * 연결 수용 조건의 사용 여부를 가져온다.
	 */
	private boolean _useAcceptCondition;
	
	/**
	 * 내부 헤더의 사용 여부를 가져온다.
	 */
	private boolean _useInternalHeader;
	
	/**
	 * 클라이언트가 리스너로 연결됐을 때 실행한다.
	 */
	public EventHandler<Void> connected;
	
	/**
	 * 리스너와의 연결이 닫혔을 때 실행된다.
	 */
	public EventHandler<TcpClient> disconnected;
	
	/**
	 * 데이터 송수신에 오류가 감지됐을 때 실행된다.
	 */
	public EventHandler<Exception> error;
	
	/**
	 * 데이터 패킷을 송신했을 때 실행된다.
	 */
	public EventHandler<TransferEventArgs> sent;

	/**
	 * 전체 데이터 패킷을 수신했을 때 실행된다.
	 */
	public EventHandler<TransferEventArgs> received;
	
	/**
	 * 데이터 패킷의 일부분을 수신했을 때 실행된다.
	 */
	public EventHandler<Integer> partReceived;
	
	public TcpClient(int bufferSize, boolean useInternalHeader, boolean useAcceptCondition)
	{
		this._useInternalHeader = useInternalHeader;
		
		this._useAcceptCondition = useAcceptCondition;
		
		this._id = GuidUtil.generateGuid();
		
		this._endian = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
	}
	
	public TcpClient(String id, boolean useInternalHeader, AsynchronousSocketChannel socket, ByteOrder endian, int bufferSize) throws IOException
	{
		this._id = id;
		
		this._useInternalHeader = useInternalHeader;
		
		this._socket = socket;
		
		this._endian = endian;
		
		if (bufferSize == 0)
		{
			bufferSize = _DEFAULT_BUFFER_SIZE;
		}
		
		try
		{
			if (this._socket.getRemoteAddress() instanceof InetSocketAddress)
			{
				InetSocketAddress remoteAddress = (InetSocketAddress)this._socket.getRemoteAddress();
				
				this._address = remoteAddress.getAddress();
				this._port = remoteAddress.getPort();
			}
		}
		catch (IOException error)
		{
			throw error;
		}
	}
	
	/**
	 * 원격 호스트에 비동기적으로 연결한다.
	 * @param address 원격 호스트의 주소 정보를 설정한다.
	 */
	public void connectAsynchronous(InetSocketAddress address)
	{
		try
		{
			this._address = address.getAddress();
			this._port = address.getPort();
			
			this._socket = AsynchronousSocketChannel.open();
			
			this._socket.connect(address, this, new CompletionHandler<Void, TcpClient>()
			{
				@Override
				public void completed(Void result, TcpClient client)
				{
					// 연결 수용 조건을 사용해야 하는 경우
					if (client._useAcceptCondition)
					{
						// 연결 수용 조건을 리스너로 전송한다.
						client.sendAcceptCondition(new AcceptCondition(client._id, client._endian));
					}
				}

				@Override
				public void failed(Throwable exc, TcpClient client)
				{
					
				}
			});
		}
		catch (Exception error)
		{
			this.error.fire(error);
		}
	}
	
	/**
	 * 원격 끝점과의 연결을 비동기적으로 닫는다.
	 * @param reuseSocket 연결을 닫은 후에 소켓을 다시 사용할 수 있으면 true, 그렇지 않으면 false를 설정한다.
	 */
	public void disconnectAsynchronous(boolean reuseSocket)
	{
		try
		{
			
		
		}
		catch (Exception error)
		{
			this.error.fire(error);
		}
	}
	
	/**
	 * 비동기적으로 데이터 수신을 시작한다.
	 */
	protected void ReceiveAsynchronous()
	{
		try
		{
			StateObject packet = new StateObject(this, this._pieceBufferSize);
			
			this._socket.read(packet.getBuffer(), packet, new CompletionHandler<Integer, StateObject>()
			{
				@Override
				public void completed(Integer receivedSize, StateObject packet)
				{
					TcpClient socket = packet.getSocket();
					
					try
					{
						if (receivedSize > 0)
						{
							Boolean received = false;
							
							TransferEventArgs result = null;
							
							int receivedLength = 0;
							
							if (socket.isUseInternalHeader())
							{
								socket.afterProcessForRecevingData(packet, received, receivedLength, result);
							}
							else
							{
								socket._bufferStream = new ByteArrayOutputStream(receivedSize);
								
								socket._bufferStream.write(packet.getBuffer().array(), 0, packet.getBuffer().array().length);
								
								result = new TransferEventArgs(packet.getSocket(), packet.getHeader(), packet.getBuffer().array());
								
								received = true;
							}
							
							if (received)
							{
								socket.received.fire(result);
								
								socket.partReceived.fire(receivedLength);
							}
						}
						// 수신된 데이터 크기가 0이거나 그보다 작은 경우 소켓을 닫는다.
						else
						{
							socket.disconnectAsynchronous(false);
						}
					}
					finally
					{
						if (receivedSize > 0)
						{
							socket.ReceiveAsynchronous();
						}
					}
				}

				@Override
				public void failed(Throwable exc, StateObject packet)
				{
					TcpClient socket = packet.getSocket();
					
					socket.disconnectAsynchronous(false);
				}
			});
		}
		catch (Exception error)
		{
			this.error.fire(error);
		}
	}
	
	/**
	 * 데이터 수신에 대한 후처리를 진행한다.
	 * @param packet 수신된 데이터 패킷을 설정한다.
	 * @param received 전체 데이터의 수신 완료 여부를 참조한다.
	 * @param receivedLength 수신된 데이터의 길이를 참조한다.
	 * @param result 전송 결과 정보를 참조한다.
	 */
	protected void afterProcessForRecevingData(StateObject packet, Boolean received, Integer receivedLength, TransferEventArgs result)
	{
		receivedLength = 0;
		
		try
		{
			// 수신해야 데이터 버퍼의 오프셋을 나타낸다.
			int currentOffset = 0;
			
			// 패킷 헤더가 존재하지 않는 경우
			if (this._currentHeader != null)
			{
				HeaderParseResult headerParseResult = HeaderParseResult.Success;
				
				// 헤더를 분석한다.
				Header header = Header.ParseHeader(packet.getBuffer().array(), headerParseResult, this._endian == ByteOrder.LITTLE_ENDIAN);
				
				// 헤더 분석에 성공한 경우 데이터 수신을 준비한다.
				if (headerParseResult == HeaderParseResult.Success)
				{
					this._currentHeader = header;
					
					// 헤더가 포함되어 있는 첫번째 블록 버퍼의 데이터 시작 위치를 설정한다.
					currentOffset = this._currentHeader.getHeaderSize();
					
					switch (this._currentHeader.getPacketType())
					{
						case Raw:
						case Serialized:
						case Message:
							
							this._bufferStream = new ByteArrayOutputStream();
							
							receivedLength = this._pieceBufferSize - currentOffset >= this._currentHeader.getBodySize() ?
									this._currentHeader.getBodySize() : this._pieceBufferSize - currentOffset;
							
							break;
							
						case File:
							
							break;
					}
				}
			}
			else
			{
				switch (this._currentHeader.getPacketType())
				{
					case Raw:
					case Serialized:
					case Message:
						
						receivedLength = this._receivedOffset + this._pieceBufferSize < this._currentHeader.getBodySize() ?
								this._pieceBufferSize : this._currentHeader.getBodySize() - this._receivedOffset;
						
						break;
						
					case File:
						
						break;
				}
			}
			
			this._bufferStream.write(packet.getBuffer().array(), currentOffset, receivedLength);
			
			this._receivedOffset += receivedLength;
			
			// 전체 데이터 수신 여부를 판단한다.
			received = this._currentHeader.getPacketType() == PacketType.File ? this._receivedOffset >= this._currentHeader.getFileSize() : this._receivedOffset >= this._currentHeader.getBodySize();
			
			if (received)
			{
				this._bufferStream.reset();
				
				switch (this._currentHeader.getPacketType())
				{
					case Raw:
					case File:
						
						result = new TransferEventArgs(this, this._currentHeader, this._bufferStream.toByteArray());
						
						break;
						
					case Serialized:
						
						ByteArrayInputStream t = new ByteArrayInputStream(this._bufferStream.toByteArray());
						
						ObjectInputStream stream = new ObjectInputStream(t);
						
						Object dataObject = stream.readObject();
						
						result = new TransferEventArgs(this, this._currentHeader, dataObject);
						
						break;
						
					case Message:
						
						byte[] receivedMessageBuffer = new byte[this._currentHeader.getBodySize()];
						
						ByteArrayInputStream tt = new ByteArrayInputStream(receivedMessageBuffer);
						
						tt.read(this._bufferStream.toByteArray());
						
						String message = BitConverter.toString(receivedMessageBuffer);
						
						result = new TransferEventArgs(this, this._currentHeader, message);
						
						break;
				}
				
				this._bufferStream = null;
				this._currentHeader = null;
				this._receivedOffset = 0;
			}
		}
		catch (Exception error)
		{
			this._bufferStream = null;
			this._currentHeader = null;
			this._receivedOffset = 0;
			
			this.error.fire(error);
		}
	}
	
	/**
	 * 비동기 데이터 송신을 시작한다.
	 * @param data 송신하려는 데이터를 설정한다.
	 * @param dataType 송신하려는 데이터의 객체 타입을 설정한다.
	 * @param packetType 패킷 유형을 설정한다.
	 * @param encryptionOption 암호화 옵션을 설정한다.
	 */
	public void sendAsynchronous(byte[] data, String dataType, PacketType packetType, EncryptionOptions encryptionOption)
	{
		try
		{
			if (dataType == null)
			{
				dataType = byte[].class.getName();
			}
			
			StateObject packet = new StateObject(this);
			
			ByteArray reservedData = new ByteArray();
			
			if (this._useInternalHeader)
			{
				packet.setHeader(this.preprocessSendingData(data, dataType, packetType, encryptionOption, reservedData));
				
				packet.setBuffer(reservedData.getArray());
			}
			else
			{
				packet.setHeader(Header.generateHeader());
				
				packet.setBuffer(data);
			}
		}
		catch (Exception error)
		{
			throw error;
		}
	}
	
	/**
	 * 비동기 객체 송신을 시작한다.
	 * @param instance 송신하려는 객체의 인스턴스를 설정한다.
	 * @param encryptionOption 암호화 옵션을 설정한다.
	 * @throws IOException 송신하려는 객체의 인스턴스를 바이너리화하는 과정에서 입출력 관련 예외가 발생할 수 있다.
	 */
	public void sendObject(Object instance, EncryptionOptions encryptionOption) throws IOException
	{
		try
		{
			ByteArrayOutputStream t = new ByteArrayOutputStream();
			
			ObjectOutputStream stream = new ObjectOutputStream(t);
			
			stream.writeObject(instance);
			
			byte[] data = t.toByteArray();
			
			this.sendAsynchronous(data, instance.getClass().getName(), PacketType.Serialized, encryptionOption);
		}
		catch (Exception error)
		{
			throw error;
		}
	}
	
	/**
	 * 비동기 메시지 송신을 시작한다.
	 * @param message 송신할 메시지를 설정한다.
	 * @param encryptionOption 암호화 옵션을 설정한다.
	 */
	public void sendMessage(String message, EncryptionOptions encryptionOption)
	{
		try
		{
			byte[] data = BitConverter.getBytes(message);
			
			this.sendAsynchronous(data, String.class.getName(), PacketType.Message, encryptionOption);
		}
		catch (Exception error)
		{
			throw error;
		}
	}
	
	/**
	 * 데이터 송신에 대한 전처리를 진행한다.
	 * @param data 송신하려는 데이터를 설정한다.
	 * @param dataType 보내려는 데이터의 타입명을 설정한다.
	 * @param packetType 패킷 유형을 설정한다.
	 * @param encryptionOption 암호화 옵션을 설정한다.
	 * @param reservedData 전처리된 송신 데이터를 참조한다.
	 * @return 송신할 데이터의 헤더 정보가 반환된다.
	 */
	protected Header preprocessSendingData(byte[] data, String dataType, PacketType packetType, EncryptionOptions encryptionOption, ByteArray reservedData)
	{
		Header header = null;
		
		if (reservedData == null)
		{
			reservedData = new ByteArray();
		}
		
		try
		{
			ByteArray headerBinary = new ByteArray();
			
			// 헤더를 생성한다.
			header = Header.generateHeader(data.length, dataType, packetType, encryptionOption, headerBinary, this._endian == ByteOrder.LITTLE_ENDIAN);
			
			// 송신할 데이터를 헤더의 크기와 원본 데이터의 크기를 합한 크기로 생성한다.
			byte[] reservedDataTemp = new byte[header.getHeaderSize() + data.length];
			
			System.arraycopy(headerBinary.getArray(), 0, reservedDataTemp, 0, headerBinary.getLength());
			System.arraycopy(data, 0, reservedDataTemp, headerBinary.getLength(), data.length);
				
			reservedData.setBytes(reservedDataTemp);
		}
		catch (Exception error)
		{
			this.error.fire(error);
		}
		
		return header;
	}
	
	/**
	 * 소켓 리스너로 Accept 조건을 전송한다.
	 * @param acceptCondition 전송할 Accept 조건을 설정한다.
	 */
	private void sendAcceptCondition(AcceptCondition acceptCondition)
	{
		try
		{
			byte[] acceptConditionBinary = acceptCondition.toByteArray();
			
			ByteBuffer buffer = ByteBuffer.allocate(acceptConditionBinary.length);
			
			this._socket.write(buffer);
		}
		catch (IOException error)
		{
			this.error.fire(error);
		}
	}
	
	public String getId()
	{
		return this._id;
	}

	public AsynchronousSocketChannel getSocket()
	{
		return this._socket;
	}

	public InetAddress getAddress()
	{
		return this._address;
	}

	public int getPort()
	{
		return this._port;
	}

	public ByteArrayOutputStream getBufferStream()
	{
		return this._bufferStream;
	}

	public ByteOrder getEndian()
	{
		return this._endian;
	}

	public boolean isUseInternalHeader()
	{
		return this._useInternalHeader;
	}
}